/*
 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.awt.dnd;

import java.util.TooManyListenersException;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.datatransfer.FlavorMap;
import java.awt.datatransfer.SystemFlavorMap;
import javax.swing.Timer;
import java.awt.peer.ComponentPeer;
import java.awt.peer.LightweightPeer;
import java.awt.dnd.peer.DropTargetPeer;


/**
 * The <code>DropTarget</code> is associated
 * with a <code>Component</code> when that <code>Component</code>
 * wishes
 * to accept drops during Drag and Drop operations.
 * <P>
 * Each
 * <code>DropTarget</code> is associated with a <code>FlavorMap</code>.
 * The default <code>FlavorMap</code> hereafter designates the
 * <code>FlavorMap</code> returned by <code>SystemFlavorMap.getDefaultFlavorMap()</code>.
 *
 * @since 1.2
 */

public class DropTarget implements DropTargetListener, Serializable {

  private static final long serialVersionUID = -6283860791671019047L;

  /**
   * Creates a new DropTarget given the <code>Component</code>
   * to associate itself with, an <code>int</code> representing
   * the default acceptable action(s) to
   * support, a <code>DropTargetListener</code>
   * to handle event processing, a <code>boolean</code> indicating
   * if the <code>DropTarget</code> is currently accepting drops, and
   * a <code>FlavorMap</code> to use (or null for the default <CODE>FlavorMap</CODE>).
   * <P>
   * The Component will receive drops only if it is enabled.
   *
   * @param c The <code>Component</code> with which this <code>DropTarget</code> is associated
   * @param ops The default acceptable actions for this <code>DropTarget</code>
   * @param dtl The <code>DropTargetListener</code> for this <code>DropTarget</code>
   * @param act Is the <code>DropTarget</code> accepting drops.
   * @param fm The <code>FlavorMap</code> to use, or null for the default <CODE>FlavorMap</CODE>
   * @throws HeadlessException if GraphicsEnvironment.isHeadless() returns true
   * @see java.awt.GraphicsEnvironment#isHeadless
   */
  public DropTarget(Component c, int ops, DropTargetListener dtl,
      boolean act, FlavorMap fm)
      throws HeadlessException {
    if (GraphicsEnvironment.isHeadless()) {
      throw new HeadlessException();
    }

    component = c;

    setDefaultActions(ops);

    if (dtl != null) {
      try {
        addDropTargetListener(dtl);
      } catch (TooManyListenersException tmle) {
        // do nothing!
      }
    }

    if (c != null) {
      c.setDropTarget(this);
      setActive(act);
    }

    if (fm != null) {
      flavorMap = fm;
    } else {
      flavorMap = SystemFlavorMap.getDefaultFlavorMap();
    }
  }

  /**
   * Creates a <code>DropTarget</code> given the <code>Component</code>
   * to associate itself with, an <code>int</code> representing
   * the default acceptable action(s)
   * to support, a <code>DropTargetListener</code>
   * to handle event processing, and a <code>boolean</code> indicating
   * if the <code>DropTarget</code> is currently accepting drops.
   * <P>
   * The Component will receive drops only if it is enabled.
   *
   * @param c The <code>Component</code> with which this <code>DropTarget</code> is associated
   * @param ops The default acceptable actions for this <code>DropTarget</code>
   * @param dtl The <code>DropTargetListener</code> for this <code>DropTarget</code>
   * @param act Is the <code>DropTarget</code> accepting drops.
   * @throws HeadlessException if GraphicsEnvironment.isHeadless() returns true
   * @see java.awt.GraphicsEnvironment#isHeadless
   */
  public DropTarget(Component c, int ops, DropTargetListener dtl,
      boolean act)
      throws HeadlessException {
    this(c, ops, dtl, act, null);
  }

  /**
   * Creates a <code>DropTarget</code>.
   *
   * @throws HeadlessException if GraphicsEnvironment.isHeadless() returns true
   * @see java.awt.GraphicsEnvironment#isHeadless
   */
  public DropTarget() throws HeadlessException {
    this(null, DnDConstants.ACTION_COPY_OR_MOVE, null, true, null);
  }

  /**
   * Creates a <code>DropTarget</code> given the <code>Component</code>
   * to associate itself with, and the <code>DropTargetListener</code>
   * to handle event processing.
   * <P>
   * The Component will receive drops only if it is enabled.
   *
   * @param c The <code>Component</code> with which this <code>DropTarget</code> is associated
   * @param dtl The <code>DropTargetListener</code> for this <code>DropTarget</code>
   * @throws HeadlessException if GraphicsEnvironment.isHeadless() returns true
   * @see java.awt.GraphicsEnvironment#isHeadless
   */
  public DropTarget(Component c, DropTargetListener dtl)
      throws HeadlessException {
    this(c, DnDConstants.ACTION_COPY_OR_MOVE, dtl, true, null);
  }

  /**
   * Creates a <code>DropTarget</code> given the <code>Component</code>
   * to associate itself with, an <code>int</code> representing
   * the default acceptable action(s) to support, and a
   * <code>DropTargetListener</code> to handle event processing.
   * <P>
   * The Component will receive drops only if it is enabled.
   *
   * @param c The <code>Component</code> with which this <code>DropTarget</code> is associated
   * @param ops The default acceptable actions for this <code>DropTarget</code>
   * @param dtl The <code>DropTargetListener</code> for this <code>DropTarget</code>
   * @throws HeadlessException if GraphicsEnvironment.isHeadless() returns true
   * @see java.awt.GraphicsEnvironment#isHeadless
   */
  public DropTarget(Component c, int ops, DropTargetListener dtl)
      throws HeadlessException {
    this(c, ops, dtl, true);
  }

  /**
   * Note: this interface is required to permit the safe association
   * of a DropTarget with a Component in one of two ways, either:
   * <code> component.setDropTarget(droptarget); </code>
   * or <code> droptarget.setComponent(component); </code>
   * <P>
   * The Component will receive drops only if it is enabled.
   *
   * @param c The new <code>Component</code> this <code>DropTarget</code> is to be associated with.
   */

  public synchronized void setComponent(Component c) {
    if (component == c || component != null && component.equals(c)) {
      return;
    }

    Component old;
    ComponentPeer oldPeer = null;

    if ((old = component) != null) {
      clearAutoscroll();

      component = null;

      if (componentPeer != null) {
        oldPeer = componentPeer;
        removeNotify(componentPeer);
      }

      old.setDropTarget(null);

    }

    if ((component = c) != null) {
      try {
        c.setDropTarget(this);
      } catch (Exception e) { // undo the change
        if (old != null) {
          old.setDropTarget(this);
          addNotify(oldPeer);
        }
      }
    }
  }

  /**
   * Gets the <code>Component</code> associated
   * with this <code>DropTarget</code>.
   * <P>
   *
   * @return the current <code>Component</code>
   */

  public synchronized Component getComponent() {
    return component;
  }

  /**
   * Sets the default acceptable actions for this <code>DropTarget</code>
   * <P>
   *
   * @param ops the default actions
   * @see java.awt.dnd.DnDConstants
   */

  public void setDefaultActions(int ops) {
    getDropTargetContext()
        .setTargetActions(ops & (DnDConstants.ACTION_COPY_OR_MOVE | DnDConstants.ACTION_REFERENCE));
  }

  /*
   * Called by DropTargetContext.setTargetActions()
   * with appropriate synchronization.
   */
  void doSetDefaultActions(int ops) {
    actions = ops;
  }

  /**
   * Gets an <code>int</code> representing the
   * current action(s) supported by this <code>DropTarget</code>.
   * <P>
   *
   * @return the current default actions
   */

  public int getDefaultActions() {
    return actions;
  }

  /**
   * Sets the DropTarget active if <code>true</code>,
   * inactive if <code>false</code>.
   * <P>
   *
   * @param isActive sets the <code>DropTarget</code> (in)active.
   */

  public synchronized void setActive(boolean isActive) {
    if (isActive != active) {
      active = isActive;
    }

    if (!active) {
      clearAutoscroll();
    }
  }

  /**
   * Reports whether or not
   * this <code>DropTarget</code>
   * is currently active (ready to accept drops).
   * <P>
   *
   * @return <CODE>true</CODE> if active, <CODE>false</CODE> if not
   */

  public boolean isActive() {
    return active;
  }

  /**
   * Adds a new <code>DropTargetListener</code> (UNICAST SOURCE).
   * <P>
   *
   * @param dtl The new <code>DropTargetListener</code> <P>
   * @throws TooManyListenersException if a <code>DropTargetListener</code> is already added to this
   * <code>DropTarget</code>.
   */

  public synchronized void addDropTargetListener(DropTargetListener dtl)
      throws TooManyListenersException {
    if (dtl == null) {
      return;
    }

    if (equals(dtl)) {
      throw new IllegalArgumentException("DropTarget may not be its own Listener");
    }

    if (dtListener == null) {
      dtListener = dtl;
    } else {
      throw new TooManyListenersException();
    }
  }

  /**
   * Removes the current <code>DropTargetListener</code> (UNICAST SOURCE).
   * <P>
   *
   * @param dtl the DropTargetListener to deregister.
   */

  public synchronized void removeDropTargetListener(DropTargetListener dtl) {
    if (dtl != null && dtListener != null) {
      if (dtListener.equals(dtl)) {
        dtListener = null;
      } else {
        throw new IllegalArgumentException("listener mismatch");
      }
    }
  }

  /**
   * Calls <code>dragEnter</code> on the registered
   * <code>DropTargetListener</code> and passes it
   * the specified <code>DropTargetDragEvent</code>.
   * Has no effect if this <code>DropTarget</code>
   * is not active.
   *
   * @param dtde the <code>DropTargetDragEvent</code>
   * @throws NullPointerException if this <code>DropTarget</code> is active and <code>dtde</code> is
   * <code>null</code>
   * @see #isActive
   */
  public synchronized void dragEnter(DropTargetDragEvent dtde) {
    isDraggingInside = true;

    if (!active) {
      return;
    }

    if (dtListener != null) {
      dtListener.dragEnter(dtde);
    } else {
      dtde.getDropTargetContext().setTargetActions(DnDConstants.ACTION_NONE);
    }

    initializeAutoscrolling(dtde.getLocation());
  }

  /**
   * Calls <code>dragOver</code> on the registered
   * <code>DropTargetListener</code> and passes it
   * the specified <code>DropTargetDragEvent</code>.
   * Has no effect if this <code>DropTarget</code>
   * is not active.
   *
   * @param dtde the <code>DropTargetDragEvent</code>
   * @throws NullPointerException if this <code>DropTarget</code> is active and <code>dtde</code> is
   * <code>null</code>
   * @see #isActive
   */
  public synchronized void dragOver(DropTargetDragEvent dtde) {
    if (!active) {
      return;
    }

    if (dtListener != null && active) {
      dtListener.dragOver(dtde);
    }

    updateAutoscroll(dtde.getLocation());
  }

  /**
   * Calls <code>dropActionChanged</code> on the registered
   * <code>DropTargetListener</code> and passes it
   * the specified <code>DropTargetDragEvent</code>.
   * Has no effect if this <code>DropTarget</code>
   * is not active.
   *
   * @param dtde the <code>DropTargetDragEvent</code>
   * @throws NullPointerException if this <code>DropTarget</code> is active and <code>dtde</code> is
   * <code>null</code>
   * @see #isActive
   */
  public synchronized void dropActionChanged(DropTargetDragEvent dtde) {
    if (!active) {
      return;
    }

    if (dtListener != null) {
      dtListener.dropActionChanged(dtde);
    }

    updateAutoscroll(dtde.getLocation());
  }

  /**
   * Calls <code>dragExit</code> on the registered
   * <code>DropTargetListener</code> and passes it
   * the specified <code>DropTargetEvent</code>.
   * Has no effect if this <code>DropTarget</code>
   * is not active.
   * <p>
   * This method itself does not throw any exception
   * for null parameter but for exceptions thrown by
   * the respective method of the listener.
   *
   * @param dte the <code>DropTargetEvent</code>
   * @see #isActive
   */
  public synchronized void dragExit(DropTargetEvent dte) {
    isDraggingInside = false;

    if (!active) {
      return;
    }

    if (dtListener != null && active) {
      dtListener.dragExit(dte);
    }

    clearAutoscroll();
  }

  /**
   * Calls <code>drop</code> on the registered
   * <code>DropTargetListener</code> and passes it
   * the specified <code>DropTargetDropEvent</code>
   * if this <code>DropTarget</code> is active.
   *
   * @param dtde the <code>DropTargetDropEvent</code>
   * @throws NullPointerException if <code>dtde</code> is null and at least one of the following is
   * true: this <code>DropTarget</code> is not active, or there is no a
   * <code>DropTargetListener</code> registered.
   * @see #isActive
   */
  public synchronized void drop(DropTargetDropEvent dtde) {
    isDraggingInside = false;

    clearAutoscroll();

    if (dtListener != null && active) {
      dtListener.drop(dtde);
    } else { // we should'nt get here ...
      dtde.rejectDrop();
    }
  }

  /**
   * Gets the <code>FlavorMap</code>
   * associated with this <code>DropTarget</code>.
   * If no <code>FlavorMap</code> has been set for this
   * <code>DropTarget</code>, it is associated with the default
   * <code>FlavorMap</code>.
   * <P>
   *
   * @return the FlavorMap for this DropTarget
   */

  public FlavorMap getFlavorMap() {
    return flavorMap;
  }

  /**
   * Sets the <code>FlavorMap</code> associated
   * with this <code>DropTarget</code>.
   * <P>
   *
   * @param fm the new <code>FlavorMap</code>, or null to associate the default FlavorMap with this
   * DropTarget.
   */

  public void setFlavorMap(FlavorMap fm) {
    flavorMap = fm == null ? SystemFlavorMap.getDefaultFlavorMap() : fm;
  }

  /**
   * Notify the DropTarget that it has been associated with a Component
   *
   * *********************************************************************
   * This method is usually called from java.awt.Component.addNotify() of
   * the Component associated with this DropTarget to notify the DropTarget
   * that a ComponentPeer has been associated with that Component.
   *
   * Calling this method, other than to notify this DropTarget of the
   * association of the ComponentPeer with the Component may result in
   * a malfunction of the DnD system.
   * *********************************************************************
   * <P>
   *
   * @param peer The Peer of the Component we are associated with!
   */

  public void addNotify(ComponentPeer peer) {
    if (peer == componentPeer) {
      return;
    }

    componentPeer = peer;

    for (Component c = component;
        c != null && peer instanceof LightweightPeer; c = c.getParent()) {
      peer = c.getPeer();
    }

    if (peer instanceof DropTargetPeer) {
      nativePeer = peer;
      ((DropTargetPeer) peer).addDropTarget(this);
    } else {
      nativePeer = null;
    }
  }

  /**
   * Notify the DropTarget that it has been disassociated from a Component
   *
   * *********************************************************************
   * This method is usually called from java.awt.Component.removeNotify() of
   * the Component associated with this DropTarget to notify the DropTarget
   * that a ComponentPeer has been disassociated with that Component.
   *
   * Calling this method, other than to notify this DropTarget of the
   * disassociation of the ComponentPeer from the Component may result in
   * a malfunction of the DnD system.
   * *********************************************************************
   * <P>
   *
   * @param peer The Peer of the Component we are being disassociated from!
   */

  public void removeNotify(ComponentPeer peer) {
    if (nativePeer != null) {
      ((DropTargetPeer) nativePeer).removeDropTarget(this);
    }

    componentPeer = nativePeer = null;

    synchronized (this) {
      if (isDraggingInside) {
        dragExit(new DropTargetEvent(getDropTargetContext()));
      }
    }
  }

  /**
   * Gets the <code>DropTargetContext</code> associated
   * with this <code>DropTarget</code>.
   * <P>
   *
   * @return the <code>DropTargetContext</code> associated with this <code>DropTarget</code>.
   */

  public DropTargetContext getDropTargetContext() {
    return dropTargetContext;
  }

  /**
   * Creates the DropTargetContext associated with this DropTarget.
   * Subclasses may override this method to instantiate their own
   * DropTargetContext subclass.
   *
   * This call is typically *only* called by the platform's
   * DropTargetContextPeer as a drag operation encounters this
   * DropTarget. Accessing the Context while no Drag is current
   * has undefined results.
   */

  protected DropTargetContext createDropTargetContext() {
    return new DropTargetContext(this);
  }

  /**
   * Serializes this <code>DropTarget</code>. Performs default serialization,
   * and then writes out this object's <code>DropTargetListener</code> if and
   * only if it can be serialized. If not, <code>null</code> is written
   * instead.
   *
   * @serialData The default serializable fields, in alphabetical order, followed by either a
   * <code>DropTargetListener</code> instance, or <code>null</code>.
   * @since 1.4
   */
  private void writeObject(ObjectOutputStream s) throws IOException {
    s.defaultWriteObject();

    s.writeObject(SerializationTester.test(dtListener)
        ? dtListener : null);
  }

  /**
   * Deserializes this <code>DropTarget</code>. This method first performs
   * default deserialization for all non-<code>transient</code> fields. An
   * attempt is then made to deserialize this object's
   * <code>DropTargetListener</code> as well. This is first attempted by
   * deserializing the field <code>dtListener</code>, because, in releases
   * prior to 1.4, a non-<code>transient</code> field of this name stored the
   * <code>DropTargetListener</code>. If this fails, the next object in the
   * stream is used instead.
   *
   * @since 1.4
   */
  private void readObject(ObjectInputStream s)
      throws ClassNotFoundException, IOException {
    ObjectInputStream.GetField f = s.readFields();

    try {
      dropTargetContext =
          (DropTargetContext) f.get("dropTargetContext", null);
    } catch (IllegalArgumentException e) {
      // Pre-1.4 support. 'dropTargetContext' was previously transient
    }
    if (dropTargetContext == null) {
      dropTargetContext = createDropTargetContext();
    }

    component = (Component) f.get("component", null);
    actions = f.get("actions", DnDConstants.ACTION_COPY_OR_MOVE);
    active = f.get("active", true);

    // Pre-1.4 support. 'dtListener' was previously non-transient
    try {
      dtListener = (DropTargetListener) f.get("dtListener", null);
    } catch (IllegalArgumentException e) {
      // 1.4-compatible byte stream. 'dtListener' was written explicitly
      dtListener = (DropTargetListener) s.readObject();
    }
  }

  /*********************************************************************/

  /**
   * this protected nested class implements autoscrolling
   */

  protected static class DropTargetAutoScroller implements ActionListener {

    /**
     * construct a DropTargetAutoScroller
     * <P>
     *
     * @param c the <code>Component</code>
     * @param p the <code>Point</code>
     */

    protected DropTargetAutoScroller(Component c, Point p) {
      super();

      component = c;
      autoScroll = (Autoscroll) component;

      Toolkit t = Toolkit.getDefaultToolkit();

      Integer initial = Integer.valueOf(100);
      Integer interval = Integer.valueOf(100);

      try {
        initial = (Integer) t.getDesktopProperty("DnD.Autoscroll.initialDelay");
      } catch (Exception e) {
        // ignore
      }

      try {
        interval = (Integer) t.getDesktopProperty("DnD.Autoscroll.interval");
      } catch (Exception e) {
        // ignore
      }

      timer = new Timer(interval.intValue(), this);

      timer.setCoalesce(true);
      timer.setInitialDelay(initial.intValue());

      locn = p;
      prev = p;

      try {
        hysteresis = ((Integer) t.getDesktopProperty("DnD.Autoscroll.cursorHysteresis")).intValue();
      } catch (Exception e) {
        // ignore
      }

      timer.start();
    }

    /**
     * update the geometry of the autoscroll region
     */

    private void updateRegion() {
      Insets i = autoScroll.getAutoscrollInsets();
      Dimension size = component.getSize();

      if (size.width != outer.width || size.height != outer.height) {
        outer.reshape(0, 0, size.width, size.height);
      }

      if (inner.x != i.left || inner.y != i.top) {
        inner.setLocation(i.left, i.top);
      }

      int newWidth = size.width - (i.left + i.right);
      int newHeight = size.height - (i.top + i.bottom);

      if (newWidth != inner.width || newHeight != inner.height) {
        inner.setSize(newWidth, newHeight);
      }

    }

    /**
     * cause autoscroll to occur
     * <P>
     *
     * @param newLocn the <code>Point</code>
     */

    protected synchronized void updateLocation(Point newLocn) {
      prev = locn;
      locn = newLocn;

      if (Math.abs(locn.x - prev.x) > hysteresis ||
          Math.abs(locn.y - prev.y) > hysteresis) {
        if (timer.isRunning()) {
          timer.stop();
        }
      } else {
        if (!timer.isRunning()) {
          timer.start();
        }
      }
    }

    /**
     * cause autoscrolling to stop
     */

    protected void stop() {
      timer.stop();
    }

    /**
     * cause autoscroll to occur
     * <P>
     *
     * @param e the <code>ActionEvent</code>
     */

    public synchronized void actionPerformed(ActionEvent e) {
      updateRegion();

      if (outer.contains(locn) && !inner.contains(locn)) {
        autoScroll.autoscroll(locn);
      }
    }

        /*
         * fields
         */

    private Component component;
    private Autoscroll autoScroll;

    private Timer timer;

    private Point locn;
    private Point prev;

    private Rectangle outer = new Rectangle();
    private Rectangle inner = new Rectangle();

    private int hysteresis = 10;
  }

  /*********************************************************************/

  /**
   * create an embedded autoscroller
   * <P>
   *
   * @param c the <code>Component</code>
   * @param p the <code>Point</code>
   */

  protected DropTargetAutoScroller createDropTargetAutoScroller(Component c, Point p) {
    return new DropTargetAutoScroller(c, p);
  }

  /**
   * initialize autoscrolling
   * <P>
   *
   * @param p the <code>Point</code>
   */

  protected void initializeAutoscrolling(Point p) {
    if (component == null || !(component instanceof Autoscroll)) {
      return;
    }

    autoScroller = createDropTargetAutoScroller(component, p);
  }

  /**
   * update autoscrolling with current cursor location
   * <P>
   *
   * @param dragCursorLocn the <code>Point</code>
   */

  protected void updateAutoscroll(Point dragCursorLocn) {
    if (autoScroller != null) {
      autoScroller.updateLocation(dragCursorLocn);
    }
  }

  /**
   * clear autoscrolling
   */

  protected void clearAutoscroll() {
    if (autoScroller != null) {
      autoScroller.stop();
      autoScroller = null;
    }
  }

  /**
   * The DropTargetContext associated with this DropTarget.
   *
   * @serial
   */
  private DropTargetContext dropTargetContext = createDropTargetContext();

  /**
   * The Component associated with this DropTarget.
   *
   * @serial
   */
  private Component component;

  /*
   * That Component's  Peer
   */
  private transient ComponentPeer componentPeer;

  /*
   * That Component's "native" Peer
   */
  private transient ComponentPeer nativePeer;


  /**
   * Default permissible actions supported by this DropTarget.
   *
   * @serial
   * @see #setDefaultActions
   * @see #getDefaultActions
   */
  int actions = DnDConstants.ACTION_COPY_OR_MOVE;

  /**
   * <code>true</code> if the DropTarget is accepting Drag &amp; Drop operations.
   *
   * @serial
   */
  boolean active = true;

    /*
     * the auto scrolling object
     */

  private transient DropTargetAutoScroller autoScroller;

    /*
     * The delegate
     */

  private transient DropTargetListener dtListener;

    /*
     * The FlavorMap
     */

  private transient FlavorMap flavorMap;

  /*
   * If the dragging is currently inside this drop target
   */
  private transient boolean isDraggingInside;
}
